/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.business;

import TimeRecording.enums.PartOfDay;
import TimeRecording.enums.PatternType;
import TimeRecording.tables.Employee;
import TimeRecording.tables.WorkingPattern;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import javax.persistence.EntityManager;

/**
 *
 * @author david
 */
public class ScheduledTime {

    private final static String PACKING = "                               ";

    private final static SimpleDateFormat DATE_FORMATTER =
            new SimpleDateFormat("dd-MMM-yyyy  ");

    private final static NumberFormat NUMBER_FORMATTER =
            new DecimalFormat("##0.00");

    private class LineOfExplanation {

        public final static String HEADER =
                "Date         Day        Day Part   Hours   Total\n";
        
        private Date date;
        private PartOfDay partOfDay;
        private double dailyHours;
        private double totalHours;

        public LineOfExplanation(Date date, PartOfDay partOfDay,
                double hours, double totalHours) {
            
            this.date = date;
            this.partOfDay = partOfDay;
            this.dailyHours = hours;
            this.totalHours = totalHours;
        }
        
        @Override
        public String toString() {
            
            return  DATE_FORMATTER.format(date) +
                    pad(DaisyChainUtilities.getDay(date), 11) +
                    pad(DaisyChainUtilities.getPartOfDay(partOfDay), 11) +                    
                    pad(NUMBER_FORMATTER.format(dailyHours), 8) +
                    NUMBER_FORMATTER.format(totalHours) +
                    "\n";
        }

        private String pad(String string, int length) {
            if (string.length() < length) {
                return string + PACKING.substring(0, length - string.length());
            } else {
                return string;
            }
        }
    }

    private GregorianCalendar hoursStartCalendar = new GregorianCalendar();
    private GregorianCalendar hoursEndCalendar = new GregorianCalendar();
    private GregorianCalendar spendForDayCalendar = new GregorianCalendar();
    private GregorianCalendar spendForWeekCalendar = new GregorianCalendar();

    private ArrayList<LineOfExplanation> explanationOfPaidHours;
    
    /**
     * Takes the supplied date range, clips it to the specified period and then
     * calculates the spend based on the clipped portion of the year.  The
     * start/end dates for clipping are fully inclusive.
     *
     * @param employee the employee whose records are to be interrogated
     * @param firstDay the first day of the leave period
     * @param firstDayPart the portion of the first day of leave
     * @param lastDate the last day of the leave period
     * @param lastDayPart the portion of the last day of leave
     * @param clippingStart the start of the clipping window (inclusive)
     * @param clippingEnd the end of the clipping window (inclusive)
     * @return
     */
    public double getClippedScheduledHours(
            EntityManager entityManager, Employee employee,
            Date firstDay, PartOfDay firstDayPart,
            Date lastDay, PartOfDay lastDayPart,
            Date clippingStart, Date clippingEnd) {

        // Clip the supplied date range to the clipping range
        if (firstDay.before(clippingStart)) {
            firstDay = clippingStart;
        }
        if (lastDay.after(clippingEnd)) {
            lastDay = clippingEnd;
        }

        // If the range has been clipped out of existence then just bail out
        if (lastDay.before(firstDay)) {
            return 0;
        }

        // Calculate the hours spent in the clipped period
        return getHours(entityManager, employee,
                firstDay, firstDayPart, lastDay, lastDayPart);
    }

    /**
     * Returns the number of scheduled paid hours for the period of time.  Half
     * days are dealt with in a simple manner - the number of hours worked for
     * the day is simply halved.  This might seem obvious but it's worth
     * thinking what would happen if someone took a morning off but was
     * scheduled to work from 11 'til 5 that day.  As stated, this method adopts
     * a simple approach; when this is unsatisfactory, it will be up to an
     * administrator or the system to make a necessary adjustment.
     *
     * @param employee the employee details to be interrogated
     * @param firstDay the first day of the period
     * @param firstDayPart the portion of the first day of the period
     * @param lastDay the last day of the period
     * @param lastDayPart the portion of the last day of the period
     * @return
     */
    public double getHours(EntityManager entityManager,
            Employee employee,
            Date firstDay, PartOfDay firstDayPart,
            Date lastDay, PartOfDay lastDayPart) {

        firstDay = DaisyChainSystem.cleanDate(firstDay);
        lastDay = DaisyChainSystem.cleanDate(lastDay);

        explanationOfPaidHours = new ArrayList<LineOfExplanation>();

        hoursStartCalendar.setTime(firstDay);
        hoursEndCalendar.setTime(lastDay);

        boolean isFirstDay = true;
        double totalHours = 0;
        double dailyHours;

        GregorianCalendar weekCal = new GregorianCalendar();

        while (hoursStartCalendar.before(hoursEndCalendar)) {
            
            dailyHours = getSpendForDay(employee,
                    hoursStartCalendar.getTime());
            
            if (isFirstDay && firstDayPart != PartOfDay.ALL_DAY) {
                dailyHours /= 2;
            }

            // Now... blue

            totalHours += dailyHours;

            if (dailyHours != 0) {
                explanationOfPaidHours.add(new LineOfExplanation(
                        hoursStartCalendar.getTime(),
                        (isFirstDay ? firstDayPart : PartOfDay.ALL_DAY),
                        dailyHours, totalHours));
            }

            isFirstDay = false;
            hoursStartCalendar.add(Calendar.DATE, 1);
        }

        dailyHours = getSpendForDay(employee, lastDay);
        
        if (lastDayPart != PartOfDay.ALL_DAY) {
            dailyHours /= 2;
        }
        totalHours += dailyHours;

        if (dailyHours != 0) {
            explanationOfPaidHours.add(new LineOfExplanation(
                    lastDay, lastDayPart, dailyHours, totalHours));
        }
        
        return totalHours;
    }

    /**
     * Get the cost of a day's holiday or sickness.  This is calculated by
     * looking at the "within date" working patterns and extracting the
     * contracted hours per day.  If the working pattern is a "weekly hours"
     * one then it is assumed that the hourly total is spread equally across
     * Monday to Friday.  Note that there can be up to three components of
     * working time for a day: fixed hours; flexi-time and semi-flexi-time.
     *
     * TODO - A further complication occurs when we have a combination of
     * patterns.  In such a scenario, we allocate flexi and semi-flexi equally
     * against all of the non-fixed days for that week.  This is a real
     * ball-ache!
     *
     * @param employee the employee for whom the calculation is being performed
     * @param date the date for which the spend is required
     * @return the hours that ought to be worked on the given day
     */
    public CategorisedTime getComplexSpendForDay(Employee employee, Date date) {

        List<WorkingPattern> patterns = employee.
                getWorkingPatterns();

        boolean fixedPatternMatch = false;
        boolean semiFlexiPatternMatch = false;
        boolean flexiPatternMatch = false;

        double totalFixed = 0;
        double totalSemiFlexi = 0;
        double totalFlexi = 0;

        // Iterate through each of the patterns
        // that might "touch" this working day
        
        for (WorkingPattern pattern : patterns) {

            // TODO: May wish to revise statement below
            // when it comes to addressing sessional work

            // Skip clocking time as it doesn't bear directly to worked hours
            if (pattern.getPatternType() == PatternType.CLOCKING_TIME) {
                continue;
            }

            // If the pattern ended before our date then bail out
            if (pattern.getPatternStartDate().after(date)) {
                continue;
            }

            // If the pattern has started after our date then bail out
            if (pattern.getPatternEndDate().before(date)) {
                continue;
            }

            boolean dailyTime =
                    (pattern.getPatternType() == PatternType.DAILY_PAID_TIME);
            
            boolean weeklyTime =
                    (pattern.getPatternType() == PatternType.WEEKLY_PAID_TIME) ||
                    (pattern.getPatternType() == PatternType.FLEXI_PAID_TIME);

            // What day of the week are we looking at?
            
            spendForDayCalendar.setTime(date);
            int day = spendForDayCalendar.get(Calendar.DAY_OF_WEEK);
            boolean matched = false;

            switch (day) {
                case Calendar.MONDAY:
                    matched = (dailyTime && pattern.getMonday()) || weeklyTime;
                    break;
                case Calendar.TUESDAY:
                    matched = (dailyTime && pattern.getTuesday()) || weeklyTime;
                    break;
                case Calendar.WEDNESDAY:
                    matched = (dailyTime && pattern.getWednesday()) || weeklyTime;
                    break;
                case Calendar.THURSDAY:
                    matched = (dailyTime && pattern.getThursday()) || weeklyTime;
                    break;
                case Calendar.FRIDAY:
                    matched = (dailyTime && pattern.getFriday()) || weeklyTime;
                    break;
                case Calendar.SATURDAY:
                    matched = dailyTime && pattern.getSaturday();
                    break;
                case Calendar.SUNDAY:
                    matched = dailyTime && pattern.getSunday();
                    break;
            }

            if (matched) {
                switch (pattern.getPatternType()) {
                    case CLOCKING_TIME:
                        break;
                    case DAILY_PAID_TIME:
                        fixedPatternMatch = true;
                        totalFixed += pattern.getDailyHours();
                        break;
                    case FLEXI_PAID_TIME:
                        flexiPatternMatch = true;
                        totalFlexi += pattern.getDailyHours();
                        break;
                    case WEEKLY_PAID_TIME:
                        semiFlexiPatternMatch = true;
                        totalSemiFlexi += pattern.getDailyHours();
                        break;
                }
            }
        }

        CategorisedTime complexSpend = new CategorisedTime();
        if (fixedPatternMatch) {
            complexSpend.setFixed(totalFixed);
        }
        if (semiFlexiPatternMatch) {
            complexSpend.setSemiFlexi(totalSemiFlexi);
        }
        if (flexiPatternMatch) {
            complexSpend.setFlexi(totalFlexi);
        }
        
        return complexSpend;
    }

    public CategorisedTime[] getComplexSpendForPeriod(
            Employee employee, DateRange range) {

        GregorianCalendar startCal = new GregorianCalendar();
        startCal.setTime(range.getFromDate());

        GregorianCalendar endCal = new GregorianCalendar();
        endCal.setTime(range.getToDate());

        int index = 0;
        CategorisedTime[] result = new CategorisedTime[range.getNumberOfDays()];

        while (!startCal.after(endCal)) {
            result[index++] = getComplexSpendForDay(employee, startCal.getTime());
            startCal.add(GregorianCalendar.DATE, 1);
        }

        return result;
    }

    /**
     * Get the cost of a day's holiday or sickness.  This is calculated by
     * looking at the "within date" working patterns and extracting the
     * contracted hours per day.  If the working pattern is a "weekly hours"
     * one then it is assumed that the hourly total is spread equally across
     * Monday to Friday.
     *
     * @param employee the employee for whom the calculation is being performed
     * @param date the date for which the spend is required
     * @return the hours that ought to be worked on the given day
     */
    public double getSpendForDay(Employee employee, Date date) {

        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(date);

        CategorisedTime complexSpend = getComplexSpendForDay(employee, date);

        Double spend = complexSpend.getTotal();

        if (spend == null) {
            return 0.0;

        } else {
            return spend;
        }
    }

    /**
     * Gets the average number of hours worked for each working day of the week
     * in which the supplied date falls.
     *
     * @param employee the employee details being queried
     * @param date the date that specifies the week of the check
     * @return the average number of hours worked on each working day
     */
    public double[] getSpendForWeek(EntityManager entityManager,
            Employee employee, Date date) {
    
        if (date != null) {
            spendForWeekCalendar.setTime(
                    DaisyChainSystem.cleanDate(date));
        } else {
            spendForWeekCalendar.setTime(
                    DaisyChainSystem.cleanDate(new Date()));
        }
        
        GregorianCalendar startCalendar = DaisyChainUtilities
                .firstDayOfWeek(spendForWeekCalendar);
        GregorianCalendar endCalendar = DaisyChainUtilities
                .lastDayOfWeek(spendForWeekCalendar);

        int index = 0;
        double[] spend = new double[7];
        while (!startCalendar.after(endCalendar)) {
            spend[index++] = getSpendForDay(employee, startCalendar.getTime());
            startCalendar.add(Calendar.DATE, 1);
        }

        return spend;
    }

    public String getExplanation() {

        StringBuffer explanation = new StringBuffer(LineOfExplanation.HEADER);
        for (LineOfExplanation dailyExplanation : this.explanationOfPaidHours) {
            explanation.append(dailyExplanation.toString());
        }
        return explanation.toString();
    }

    /**
     * This method will return an array (an element per day) representing the
     * number of hours allocated to sickness during that period.  Where there is
     * no holiday, the method will return null.  The method bases the number of
     * hours on the "ScheduledPaidHours" calculations.
     *
     * @param range
     * @return
     */
    public Double[] getPaidHoursForPeriod(Employee employee, DateRange range) {

        GregorianCalendar startCal = new GregorianCalendar();
        startCal.setTime(range.getFromDate());

        GregorianCalendar endCal = new GregorianCalendar();
        endCal.setTime(range.getToDate());

        long millisenconds =
                range.getToDate().getTime() -
                range.getFromDate().getTime();
        int days = Math.round(millisenconds / (24 * 60 * 60 * 1000));

        int index = 0;
        Double[] result = new Double[days];

        while (!startCal.after(endCal)) {

            double paidHours = getSpendForDay(employee, startCal.getTime());

            if (paidHours != 0) {
                result[index] = paidHours;
            }
            index++;

            startCal.add(GregorianCalendar.DATE, 1);
        }

        return result;
    }
}